這次第一版方便快速開發選擇了 Material-Ui 套件來當作佈景主題開發 Next.js 為主軸框架 支援SSR
前端會用到打 api的部分 使用了 apollo client
https://www.apollographql.com/
next.js 目前新版都有支援自動產出tsconfig 設定檔 只要他偵測規定目錄中 有ts檔案就會幫你自動設定好好的
佈景主題部分設定
在pages 目錄底下 新增 _documnet.tsx 他會覆蓋原來的既有的 _document.tsx檔案
因為要使用material Ui 主要在 SSR 時候 ,使用getInitialProps 去要到第一次需要的值 確保class與前端render出來的class一致保持正確性
pages/_documnet.tsx
import React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheets } from '@material-ui/core/styles';
import theme from '../src/theme';
export default class MyDocument extends Document {
render() {
return (
<Html lang="zh-TW">
<Head>
{/* PWA primary color */}
<meta name="theme-color" content={theme.palette.primary.main} />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
</Head>
<body>
<Main />
<NextScript />
<script type="text/javascript" src="/workfile/jsmpeg.min.js"></script>
<script src="https://js.tappaysdk.com/tpdirect/v5.4.0"></script>
<script src="https://pay.google.com/gp/p/js/pay.js"></script>
</body>
</Html>
);
}
}
MyDocument.getInitialProps = async (ctx) => {
const sheets = new ServerStyleSheets();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
};
};
在pages/_app.tsx中設定 ApolloProvider
export const PagePermissionContext = React.createContext({ pagePermission: {} })
interface Props { apolloClient: ApolloClient<any> }
const MyApp = (props: any) => {
const { Component, apolloClient, } = props
return (
<ApolloProvider client={apolloClient}>
<CssBaseline />
<Layout ><Component /></Layout>
</ApolloProvider>
)
}
export default withApollo(MyApp)
Next.js 中設定 ApolloClient 與 SubscriptionClient
這個部分有點繁雜 因為Next.js有SSR 要同時考慮到 呼叫時是前後端哪種模式
設定檔官方也有提供範例 可以參考,主要是httpLink他是結合很多midddleware 這部分比較繁瑣可以參考以下程式
後面章節再來細說運行內容
import { ApolloClient } from 'apollo-client'
import { setContext } from 'apollo-link-context'
import fetch from 'isomorphic-unfetch'
import { createUploadLink } from 'apollo-upload-client'
import config from '../config'
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { WebSocketLink } from 'apollo-link-ws';
import { InMemoryCache, NormalizedCacheObject } from 'apollo-cache-inmemory';
import { split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import ws from 'ws';
let apolloClient: any = null
const WS_URI = `wss://clawfun.online/graphql`;
const cache = new InMemoryCache();
if (!process.browser) { global["fetch"] = fetch }
const ssrMode = (typeof window === 'undefined');
function create(initialState: any, { getToken }: { getToken: any }) {
const token = getToken()
let link: any, linkServer: any, linkClient: any, wsLink: any
const httpLink = createUploadLink({ uri: `${config.serverURL()}/graphql`, credentials: 'include' })
const authLink = setContext((_, { headers }) => { const token = getToken(); return { headers: { ...headers, authorization: token ? `Bearer ${token}` : '' } } })
try {
if (!ssrMode) { //client part
wsLink = new SubscriptionClient(WS_URI, { reconnect: true, connectionParams: { headers: { authorization: token ? `Bearer ${token}` : '' } } });
linkClient = split(({ query }) => {
const definition = getMainDefinition(query);
return (definition.kind === 'OperationDefinition' && definition.operation === 'subscription');
}, wsLink, httpLink); console.log('linkClient');
} else {
linkServer = authLink.concat(httpLink); console.log('linkServer');
}
} catch (err) { console.log('err', err); }
const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({
connectToDevTools: process.browser,
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
link: ssrMode ? linkServer : linkClient,
cache: new InMemoryCache({ dataIdFromObject: (object: any) => object["key"] || null }).restore(initialState || {})
})
return client
}
export default function initApollo(initialState: any, options: any) {
if (!ssrMode) { return create(initialState, options) }
if (!apolloClient) { apolloClient = create(initialState, options) }
return apolloClient
}
前端讀取娃娃機的影像程式片段 roomCamera 元件
使用套件 為 https://github.com/phoboslab/jsmpeg
使用要先在 _document設定 讓他append在window底下
<script type="text/javascript" src="/workfile/jsmpeg.min.js"></script>
這邊在 useLayoutEffect 新增一個 window.JSMpeg.Player 物件 + return時可以釋放資源 請參考下面程式碼
const WEBSITE_STREAM_HOST = process.env.WEBSITE_STREAM_HOST ? process.env.WEBSITE_STREAM_HOST : '192.168.0.27'
function onStreamReady() { return { firstFrame: true } }
var player1: any; var player2: any;
useLayoutEffect(() => {
let canvas1 = document.getElementById('video-canvas1'); let canvas2 = document.getElementById('video-canvas2');
let urlBack = rtmpUrl1;
let urlFront = rtmpUrl2;
player1 = new window.JSMpeg.Player(urlBack, { canvas: canvas1, onStreamReady });
player2 = new window.JSMpeg.Player(urlFront, { canvas: canvas2, onStreamReady });
player1.play(); player2.play();
return () => { try { player1 = null; player2 = null; } catch (err) { console.log('err', err) } }
}, []);
Apollo Subscription 這邊使用hook useSubscription 監聽server傳來的資料.配合useState server資料傳來事後再重新render
以下片段是監聽server 例如A玩家發出投幣請求,機器後端驗證無誤就會發改變通知到前端,並且帶uuid回來如果是正在玩的玩家就設定開局,並改變遊玩會用到的參數state, 其他人就設定基本無遊玩 並通知目前有多少人在正在排隊
自己如果有在排隊序列中 也會告知排在第幾個
大致寫法如下 定義一組 Subscription Gql
export const machineRefreshSubscriptionGql = gql`subscription machineRefresh($machineId:Int){machineRefresh(machineId:$machineId){
currentMemberUuid, machineId ,machineStatus,
machineMemberQueue{
machine{id }
member{id,uuid}
}
}}`
透過socket 在娃娃機房間的每個人 依照狀況改變state 觸發 render
useSubscription(machineRefreshSubscriptionGql, {
variables: { machineId }, onSubscriptionData: ({ client, subscriptionData: { data } }) => {
if (data?.machineRefresh?.currentMemberUuid === uuid) {
setRoomState({ ...roomState, isPlayer: true, machineStatus: data?.machineRefresh?.machineStatus, queueList: data?.machineRefresh?.machineMemberQueue })
} else {
setRoomState({ ...roomState, isPlayer: false, machineStatus: data?.machineRefresh?.machineStatus, queueList: data?.machineRefresh?.machineMemberQueue })
}
}
})
前端登入功能 , 以下是判斷是否有無登入的graph document
export const meQuery = gql`
query meQuery{
meQuery{
id
name
account
picture
email
uuid
....略
}
因為不需要每次都判斷是否已經登入 ,判斷一次後就cache狀況 所以 fetchPolicy設定cache-and-network
_app是初始 apollo地方 而判斷切入點放在layout
<ApolloProvider client={apolloClient}>
<ThemeProvider theme={theme}>
<CssBaseline />
<Layout assignLang={assignLang} ><Component /></Layout>
</ThemeProvider>
</ApolloProvider>
const LayoutTemplate = (props: any) => {
const result = useQuery(meQuery, { fetchPolicy: 'cache-and-network' })
const { loading, error, data } = result;
if (loading) { return <div>loading</div> };
if (error) { return <div>error</div> };
const loginInfo = data['meQuery']
return <MainContainer />
}
而fb登入與google登入部分則直接對應express api 對應 passport使用
後端express api
app.get('/fblogin', passport.authenticate('facebook', { scope: 'email' }))
以上介紹了 前端會用到的功能 主要包括
1.apollo client 設定
2.wss://讀取影像串流 jsmpeg 設定
3.apollo Subscription 設定
4.登入介紹